package bf.runner;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Stack;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bf.util.BFProgram;

import com.sun.org.apache.bcel.internal.Constants;

public class BF2BytecodeRunner2 extends ClassLoader implements Constants {
	
	private static final String BASE = "bf/util/LocalBF";

	public static void main (final String args[]) throws Exception {
		long time1 = System.currentTimeMillis();
		run(new FileReader(args[0]));
		long time2 = System.currentTimeMillis();
		System.out.println();
		System.out.println(time2 - time1 + " ms");
	}

	private static void run(Reader reader) throws IOException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		Class bfc = load(compileInfo(reader));
		run(bfc);
	}

	private static byte[] compileInfo(Reader reader) throws IOException {
		byte[] code = compile(reader);
		System.out.println("Compiled code size: " + code.length + " bytes");
		return code;
	}
	
	private static byte[] compile(Reader reader) throws IOException {
		ClassWriter cw = new ClassWriter(0);
		cw.visit(Opcodes.V1_5, ACC_PUBLIC,"bf", null, BASE, null);

		{
			Stack<Label> labelStack = new Stack<Label>();
			MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "run", "()V", null, null);
			mv.visitCode();

			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitIntInsn(SIPUSH, 10000);
			mv.visitIntInsn(NEWARRAY, T_BYTE);
			mv.visitVarInsn(ASTORE, 0);
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitInsn(ICONST_0);
			mv.visitVarInsn(ISTORE, 1);
			Label l2 = new Label();
			mv.visitLabel(l2);
			
			int read;
			while((read = reader.read()) != -1) {
				char cmd = (char)read;
				switch(cmd) {
					case '>':
						mv.visitIincInsn(1, 1);
						break;
					case '<':
						mv.visitIincInsn(1, -1);
						break;
					case '+':
						mv.visitVarInsn(ALOAD, 0);
						mv.visitVarInsn(ILOAD, 1);
						mv.visitInsn(DUP2);
						mv.visitInsn(BALOAD);
						mv.visitInsn(ICONST_1);
						mv.visitInsn(IADD);
						mv.visitInsn(I2B);
						mv.visitInsn(BASTORE);
						break;
					case '-':
						mv.visitVarInsn(ALOAD, 0);
						mv.visitVarInsn(ILOAD, 1);
						mv.visitInsn(DUP2);
						mv.visitInsn(BALOAD);
						mv.visitInsn(ICONST_1);
						mv.visitInsn(ISUB);
						mv.visitInsn(I2B);
						mv.visitInsn(BASTORE);
						break;
					case '.':
						mv.visitVarInsn(ALOAD, 0);
						mv.visitVarInsn(ILOAD, 1);
						mv.visitInsn(BALOAD);
						mv.visitMethodInsn(INVOKESTATIC, BASE, "outASCII", "(B)V");
						break;
					case '[':
						Label jmpToLabel = new Label();
						mv.visitJumpInsn(GOTO, jmpToLabel);
						Label jmpBackLabel = new Label();
						mv.visitLabel(jmpBackLabel);
						mv.visitFrame(Opcodes.F_APPEND,2, new Object[] {"[B", Opcodes.INTEGER}, 0, null);
						labelStack.add(jmpToLabel);
						labelStack.add(jmpBackLabel);
						break;
					case ']':
						jmpBackLabel = labelStack.pop();
						jmpToLabel = labelStack.pop();
						mv.visitLabel(jmpToLabel);
						mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
						mv.visitVarInsn(ALOAD, 0);
						mv.visitVarInsn(ILOAD, 1);
						mv.visitInsn(BALOAD);
						mv.visitJumpInsn(IFNE, jmpBackLabel);
						break;
					default:
//						throw new RuntimeException("Unknown command!");
//						System.out.println("Ignore: " + cmd);
						continue;
				}
			}
			mv.visitInsn(RETURN);
			Label l114 = new Label();
			mv.visitLabel(l114);
			mv.visitLocalVariable("cell", "[B", null, l1, l114, 0);
			mv.visitLocalVariable("pointer", "I", null, l2, l114, 1);
			mv.visitMaxs(4, 2);
			mv.visitEnd();
		}

		cw.visitEnd();
		byte[] code = cw.toByteArray();
		return code;
	}
	
	private static Class load(byte[] code) {
		BF2BytecodeRunner2 loader = new BF2BytecodeRunner2();
		Class exampleClass = loader.defineClass("bf", code, 0, code.length);
		return exampleClass;
	}
	
	private static void run(Class bfClass) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		Method main = bfClass.getMethods()[0];
		main.invoke(null);
	}
	
	private static void save(byte[] code) throws IOException {
		FileOutputStream fos = new FileOutputStream("bf.class");
		fos.write(code);
		fos.close();
	}
}
